generated code: keep source.py parseable/runnable on a pre-3.13 target (PyPy 3.11) - #256
Merged
Conversation
fusil's own floor is 3.13, but that constrains the *runner*. The generated script is
executed by --python, which is routinely older -- PyPy 3.11 is a fuzzing target in its
own right. Two constructs leaked into the emitted script and killed every session
before any fuzzing happened:
- `f"{expr!r }"` (a space between the conversion and the closing brace) is PEP 701
syntax, so the whole script is a SyntaxError on a 3.11 target. Three emitter sites in
write_python_code.py; tightening them to `{expr!r}` leaves the printed text identical.
- `types.CapsuleType` is 3.13+, read unguarded in the tricky-objects prelude. On PyPy
3.11 every session died at that line with AttributeError. Guarded the same way the
neighbouring `types.GenericAlias` already is; the `if tricky_capsule:` use site below
it already expects a falsy value.
Both failure modes are invisible from the outside: the run looks *clean* (no crashes
kept) because nothing ever ran. Measured on PyPy 3.11.15 / 7.3.23, a 12-session run went
from 0 fuzzed calls to thousands of lines of real activity per session.
tests/python/test_target_py311_compat.py locks both in (each fails if its fix is
reverted). Golden snapshot regenerated for the intentional output change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhcpLoyjUWLbETGZnA9boj
Nortaq-PlayNexus
pushed a commit
to Nortaq-PlayNexus/fusil
that referenced
this pull request
Sep 4, 2026
`gc.set_threshold` is CPython-only. The prelude runs on the TARGET, so on PyPy the
unguarded call raised AttributeError at MODULE LEVEL, at line 55, before a single
target call:
import gc
gc.set_threshold(1, 1, 1)
AttributeError: module 'gc' has no attribute 'set_threshold'
The failure mode is the bad part: the sessions do not look broken, they look CLEAN.
A 43 368-session PyPy fleet ran to completion reporting zero crashes, zero timeouts,
zero cpu-load kills and 0.3-second sessions, and nothing in any log said why. Every
one of those sessions was dead on arrival.
Verified on a real script out of that fleet: before the guard it produces 6 lines of
output and exits 1; after, 500 lines and it is fuzzing (`--- Fuzzing 40 functions in
traceback ---`).
The flag stays a no-op rather than an error on such a target -- it is a tuning knob,
not a request, and PyPy's GC is not generational in CPython's sense so there is
nothing to coerce.
Two tests: one asserts the call is inside a try/except AttributeError by walking the
AST rather than matching text, and one execs the emitted snippet against a gc module
with no set_threshold, which is what PyPy looks like from the prelude's point of view.
Same shape as the pre-3.13 target-compat break fixed in devdanzin#256. The general rule that
keeps being relearned: anything CPython-specific in the prelude must be guarded,
because the prelude runs on the target.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
fusil's
requires-pythonfloor of 3.13 constrains the runner. The generatedsource.pyis executed by--python, which is routinely older or alternative — PyPy 3.11is a fuzzing target in its own right.
Two 3.12+/3.13+ constructs leaked into the emitted script and aborted every session
before any fuzzing happened:
f"{expr!r }"— a space between the conversion and the closing brace is PEP 701syntax (3.12+). On a 3.11 target the entire script is a
SyntaxError, so nothingparses. Three emitter sites in
write_python_code.py; tightening them to{expr!r}leaves the printed text byte-identical.
types.CapsuleType— 3.13+, read unguarded in the tricky-objects prelude. On PyPy3.11 every session died at that line with
AttributeError. Now guarded exactly the waythe neighbouring
types.GenericAliasalready is; theif tricky_capsule:use siteimmediately below already expects a falsy value.
Why this is worth a test
Both failure modes are invisible from the outside: the run looks clean — no crashes,
no kept dirs — because nothing ever ran. A 12-session PyPy run reported zero findings while
in fact zero calls had been fuzzed.
Measured on PyPy 3.11.15 / 7.3.23 (
--discover-in-target, CPython 3.14 runner): sessionswent from dying in the prelude to thousands of lines of real activity each.
Tests
tests/python/test_target_py311_compat.py— verified each test fails when its fix isreverted:
typesshim withCapsuleTyperemoved, assertingit survives and yields
None;fusil/for a conversion field followed by whitespace, andchecks the golden snapshot for the same.
Full suite green (1247 tests);
ruff check+ruff format --checkclean. Golden snapshotregenerated for the intentional output change.
🤖 Generated with Claude Code